package com.hyj.dev.res.proxy;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.hyj.dev.res.proxy.entity.ParseContext;
import com.hyj.dev.res.proxy.entity.ResourceResponse;
import com.hyj.dev.res.proxy.properties.AppProperties;
import com.hyj.dev.res.proxy.properties.ProxyRule;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.brotli.dec.BrotliInputStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;

/**
 * @author HeYuJie
 * @date 2023/2/17
 */
@Slf4j
@Controller
@RequestMapping
public class MainController {

    private final ObjectMapper objectMapper = new ObjectMapper();
    {
        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }


    @Autowired
    private AppProperties appProperties;


    private final ParserContext parserContext = new TemplateParserContext();
    private final SpelExpressionParser parser = new SpelExpressionParser();

    @PostMapping(value = "/debug/*/**", consumes = MediaType.APPLICATION_JSON_VALUE)
    @org.springframework.web.bind.annotation.ResponseBody
    public String debug(@RequestBody ProxyRule proxyRule, HttpServletRequest request, HttpServletResponse response) throws IOException {

        ParseContext parseContext = buildParseContext(proxyRule, request);

        String outStr = buildDebugText(parseContext, proxyRule);
        System.out.println(outStr);

        return outStr;
    }

    @RequestMapping("/proxy/{id}/**")
    public void proxy2(@PathVariable("id") String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
        boolean debug = BooleanUtil.toBoolean(request.getParameter("debug"));

        List<ProxyRule> proxyRules = appProperties.getProxyRules();
        ProxyRule proxyRule = proxyRules.stream().filter(item -> item.getId().equals(id)).findFirst().orElseThrow(()-> new RuntimeException("找不到规则："+id));

        // 验证参数
        String[] params = proxyRule.getParams();
        if(params != null){
            for (String param : params) {
                if(StrUtil.isEmpty(request.getParameter(param))){
                    throw new RuntimeException("参数【"+param+"】是必须的");
                }
            }
        }

        ParseContext parseContext = buildParseContext(proxyRule, request);

        if(debug){

            PrintWriter writer = response.getWriter();
            String outStr = buildDebugText(parseContext, proxyRule);
            System.out.println(outStr);

            response.setContentType(MediaType.TEXT_PLAIN_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            writer.print(outStr);
            IoUtil.close(writer);
            return;
        }

        ServletOutputStream resOut = response.getOutputStream();
        ResourceResponse res = new ResourceResponse();
        getLocal(res, proxyRule, parseContext);

        if(!HttpStatus.OK.equals(res.getState()) || res.getData() == null){ // 没有请求体
            getRemote(res, proxyRule, parseContext, request);
        }

        if(!HttpStatus.OK.equals(res.getState())){
            response.setContentType(MediaType.TEXT_PLAIN_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            switch (res.getState()){
                case NO_CONTENT:
                case NOT_FOUND:
                    response.setStatus(HttpStatus.NOT_FOUND.value());
                    resOut.println("not find");
                    break;
                case INTERNAL_SERVER_ERROR:
                    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                    resOut.println("error");
                    break;
            }
            return;
        }

        response.setContentType(res.getContentType());
        IoUtil.copy(res.getData(), resOut);
        IoUtil.close(res.getData());
        IoUtil.close(resOut);

    }

    private ParseContext buildParseContext(ProxyRule proxyRule, HttpServletRequest request){
        // 解析路径参数
        String uri = request.getRequestURI();
        String[] paths = uri.split("/");
        // 去除第一个和第二个元素
        paths = Arrays.copyOfRange(paths, 2, paths.length);

        ParseContext parseContext = new ParseContext();
        parseContext.setRule(proxyRule);
        parseContext.setParam(request.getParameterMap());
        parseContext.setPath(paths);

        int index = paths[paths.length-1].lastIndexOf('.');
        String suffix = "";
        if(index != -1){
            suffix = paths[paths.length-1].substring(index+1);
        }
        String subUri = substring(uri, "/", proxyRule.getSubUriIndex());
        parseContext.setSubUri(subUri);
        parseContext.setUri(uri);
        parseContext.setSuffix(suffix);

        Map<String, String[]> queryParams = new HashMap<>(request.getParameterMap());
        String queryString = UrlQuery.of(queryParams).build(StandardCharsets.UTF_8);
        parseContext.setQueryParams(queryString);

        return parseContext;
    }

    private String buildDebugText(ParseContext parseContext, ProxyRule proxyRule) throws IOException {

        String jsonStr = objectMapper.writeValueAsString(parseContext);
        String url = (String) parser.parseExpression(proxyRule.getUrl(), parserContext).getValue(parseContext);

        return "\n\n# ===== DEBUG =====\n" + "## EL Variable\n" +
                "```json\n" +
                jsonStr +
                "\n```\n" +
                "## Request URL\n" +
                "[" + url + "](" + url + ")\n";
    }

    private OkHttpClient getClient(ProxyRule proxyRule) {
        OkHttpClient client = proxyRule.getClient();
        if(client == null){
            OkHttpClient.Builder builder = new OkHttpClient().newBuilder()
                    .callTimeout(appProperties.getTimeout(), TimeUnit.SECONDS)
                    .connectTimeout(appProperties.getTimeout(), TimeUnit.SECONDS)
                    .readTimeout(appProperties.getTimeout(), TimeUnit.SECONDS);

            if(proxyRule.getProxyType() != null && StrUtil.isNotEmpty(proxyRule.getProxyHost())){
                builder.proxy(new Proxy(proxyRule.getProxyType(), new InetSocketAddress(proxyRule.getProxyHost(), proxyRule.getProxyPort())));
            }

            client = builder.build();
            proxyRule.setClient(client);
        }

        return client;
    }


    private void getLocal(ResourceResponse res, ProxyRule proxyRule, ParseContext parseContext){
        try {
            if(StrUtil.isEmpty(proxyRule.getCachePath())){
                res.setState(HttpStatus.NO_CONTENT);
                return;
            }

            String path = appProperties.getBasePath() + parser.parseExpression(proxyRule.getCachePath(), parserContext).getValue(parseContext);

            File cache = new File(path);
            res.setLocalFile(cache);

            // 设置响应的mine
            String mimeType = Files.probeContentType(cache.toPath());
            if(cache.exists()){
                res.setContentType(mimeType);
                res.setState(HttpStatus.OK);
                res.setData(Files.newInputStream(cache.toPath()));
            }
        } catch (Exception e){
            log.error("本地读取错误", e);
            res.setState(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    private void getRemote(ResourceResponse res, ProxyRule proxyRule, ParseContext parseContext, HttpServletRequest request){

            // 需要发请求
            String url = (String) parser.parseExpression(proxyRule.getUrl(), parserContext).getValue(parseContext);
            if(url == null){
                res.setState(HttpStatus.INTERNAL_SERVER_ERROR);
                log.error("请求url不能为空");
                return;
            }
            OkHttpClient client = getClient(proxyRule);

            Request.Builder getRequest = new Request.Builder()
                    .method("GET",null)
                    .url(url);

            Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()){
                String name = headerNames.nextElement();
                if(name.equals("host")){
                    continue;
                }
                getRequest.header(name, request.getHeader(name));
            }

            if(CollUtil.isNotEmpty(proxyRule.getHeader())){
                Map<String, String> ruleHeader = proxyRule.getHeader();
                for (Map.Entry<String, String> entry : ruleHeader.entrySet()) {
                    getRequest.header(entry.getKey(), entry.getValue());
                }
            }


            try {
                Response execute = client.newCall(getRequest.build()).execute();
                if (execute.isSuccessful()){
                    ResponseBody body = execute.body();
                    if(body == null){
                        res.setState(HttpStatus.INTERNAL_SERVER_ERROR);
                        log.error("没有响应体");
                        return;
                    }

                    // 处理gzip
                    String header = execute.header("content-encoding");
                    if("gzip".equals(header)){
                        res.setData(new GZIPInputStream(body.byteStream()));
                    } else if("br".equals(header)){
                        res.setData(new BrotliInputStream(body.byteStream()));
                    } else {
                        res.setData(body.byteStream());
                    }

                    // 是否需要写入本地
                    if(StrUtil.isNotEmpty(proxyRule.getCachePath())){

                        new File(res.getLocalFile(), "../").mkdirs();
                        // 写出到本地
                        //noinspection VulnerableCodeUsages
                        FileUtil.writeFromStream(res.getData(), res.getLocalFile(), true);
                        res.setData(new FileInputStream(res.getLocalFile()));
                    }
                    okhttp3.MediaType mediaType = body.contentType();
                    if(mediaType == null){
                        res.setContentType("application/octet-stream");
                    } else {
                        res.setContentType(mediaType.toString());
                    }
                    res.setState(HttpStatus.OK);
                } else {
                    log.error("请求失败, {}, {}\n{}", execute.code(), url, execute.message());
                    res.setState(HttpStatus.INTERNAL_SERVER_ERROR);
                }
            } catch (IOException e) {
                log.error("http get 请求失败--"+url,e);
                res.setState(HttpStatus.INTERNAL_SERVER_ERROR);
            }
    }

    private String substring(String str, String sub, int count){
        int idx = -1;
        for(int i=0; i<count; i++){
            idx = str.indexOf(sub, idx+1);
        }
        if(idx == -1){
            return "";
        }
        return str.substring(idx+1);
    }

}
